#include "Preprocessor.hpp"
#include "InputStream.hpp"
#include "ParsingErrorException.hpp"

namespace {

class OutputSession
{
public:
	OutputSession(std::vector<char> * buffer, char * data, int maxSize)
		: data_(data)
		, left_(maxSize)
		, done_(0)
		, buffer_(buffer)
	{
		size_t n = std::min(size_t(left_), buffer_->size());
		if(n > 0)
		{
			for(size_t k=0; k<n; ++k)
			{
				do_putch(buffer_->at(k));
			}
			buffer_->erase(buffer_->begin(), buffer_->begin() + n);
		}
	}

	~OutputSession()
	{
	}

	bool done() const { return left_ <= 0; }
	int processed() const {return done_; }
	int remaining() const { return left_; }

	void put(char c)
	{
		if(left_ > 0) do_putch(c);
		else buffer_->push_back(c);
	}
private:
	char * data_;
	int left_;
	int done_;
	std::vector<char> * buffer_;

	void do_putch(char c)
	{
		*(data_++) = c;
		--left_; ++done_;
	}
};

} // anonymous-namespace

Preprocessor::Preprocessor(InputStream * stream)
: stream_(stream)
, outBuffer_()
, state_(UnitializedText)
, codeBlockState_(UninitializedCodeBlock)
, textBlocks_()
{

}

Preprocessor::~Preprocessor()
{

}

std::pair<String, SourceLocation> Preprocessor::takeTextBlock()
{
	std::pair<String, SourceLocation> result = textBlocks_.front();
	textBlocks_.pop_front();
	return result;
}

SourcePosition Preprocessor::takePosition()
{
	SourcePosition pos = positions_.front();
	positions_.pop_front();
	return pos;
}

int Preprocessor::read(char *data, int maxCount)
{
	OutputSession outputSession(&outBuffer_, data, maxCount);
	
	String textBuffer; //buffer for collecting text block characters

	while(state_ != EndOfFile && !outputSession.done())
	{
		InputGetter g(stream_);

		bool const eof = g.eof();
		char const c = g.ch();

		switch(state_)
		{
		case UnitializedText:
			textBuffer.clear();
			stream_->startParsing();
			state_ = Text;
			/* continue to Text */
		case Text:
			if(eof || c == '@')
			{
				if(!textBuffer.empty())
				{
					textBlocks_.push_back( std::make_pair(textBuffer, stream_->consumedLocation()) );
					textBuffer.clear();
					outputSession.put('@');
					outputSession.put('T');
					outputSession.put(';');
				}
				state_ = eof ? EndOfFile : AtSign;
			}
			else
			{
				textBuffer.push_back(c);
			}
			break;
		case AtSign:
			if(eof)
			{
				throw ParsingErrorException(g.position(), "Unexpected end of file");
			}
			if(c == '@')
			{
				codeBlockState_ = EndOfLineCodeBlock;
				state_ = UninitializedCodeBlock;
			}
			else if(c =='{')
			{
				codeBlockState_ = MultiLineCodeBlock;
				state_ = UninitializedCodeBlock;
			}
			else if(c == '/')
			{
				state_ = EndOfLineComment;
			}
			else if(c == '*')
			{
				state_ = MultiLineComment;
			}
			else
			{
				g.discard();
				codeBlockState_ = InLineCodeBlock;
				state_ = UninitializedCodeBlock;
			}
			break;
		case EndOfLineComment:
			if(eof)
			{
				throw ParsingErrorException(g.position(), "Unexpected end of file");
			}
			else if(c == '\n')
			{
				state_ = UnitializedText;
			}
			else
			{
				/* remain in EndOfLineComment state */
			}
			break;
		case MultiLineComment:
			if(eof)
			{
				throw ParsingErrorException(g.position(), "Unexpected end of file");
			}
			else if(c == '*')
			{
				state_ = MaybeEndOfComment;
			}
			else
			{
				/* remain in MultiLineComment */
			}
			break;
		case MaybeEndOfComment:
			if(eof)
			{
				throw ParsingErrorException(g.position(), "Unexpected end of file");
			}
			else if(c == '@')
			{
				state_ = UnitializedText;
			}
			else if(c == '{')
			{
				/* remain in MaybeEndOfComment state */
			}
			else
			{
				state_ = MultiLineComment;
			}
			break;
		case UninitializedCodeBlock:
			assert(codeBlockState_ != UninitializedCodeBlock);
			g.discard();
			positions_.push_back( stream_->position() );
			outputSession.put('@');
			outputSession.put('P');
			state_ = codeBlockState_;
			codeBlockState_ = UninitializedCodeBlock;
			break;
		case InLineCodeBlock:
			if(eof)
			{
				throw ParsingErrorException(g.position(), "Unexpected end of file");
			}
			else if(c == '@')
			{
				state_ = UnitializedText;
				outputSession.put(';');
			}
			else if(c == '\n')
			{
				throw ParsingErrorException(g.position(), "Unterminated in-line code block");
			}
			else if(c == '{')
			{
				throw ParsingErrorException(g.position(), "Unexpected character '{'");
			}
			else if(c == '}')
			{
				throw ParsingErrorException(g.position(), "Unexpected character '}'");
			}
			else
			{
				outputSession.put(c);
			}
			break;
		case EndOfLineCodeBlock:
			if(eof)
			{
				state_ = UnitializedText;
				outputSession.put(';');
			}
			else if(c == '@')
			{
				throw ParsingErrorException(g.position(), "Unexpected '@' character inside end-of-line code block");
			}
			else if(c == '{')
			{
				throw ParsingErrorException(g.position(), "Unexpected character '{'");
			}
			else if(c == '}')
			{
				throw ParsingErrorException(g.position(), "Unexpected character '}'");
			}
			else if(c == '\n')
			{
				state_ = UnitializedText;
				outputSession.put(';');
				outputSession.put(c);
			}
			else
			{
				outputSession.put(c);
			}
			break;
		case MultiLineCodeBlock:
			if(eof)
			{
				throw ParsingErrorException(g.position(), "Unexpected end of file");
			}
			else if(c == '@')
			{
				throw ParsingErrorException(g.position(), "Unexpected '@' character inside multi-line code block");
			}
			else if(c == '{')
			{
				throw ParsingErrorException(g.position(), "Unexpected character '{'");
			}
			else if(c == '}')
			{
				state_ = MaybeEndOfBlock;
			}
			else if(c == '\n')
			{
				outputSession.put(c);
			}
			else
			{
				outputSession.put(c);
			}
			break;
		case MaybeEndOfBlock:
			if(c == '@')
			{
				state_ = UnitializedText;
				outputSession.put(';');
			}
			else
			{
				throw ParsingErrorException(g.position(), "Invalid multi-line block terminator");
			}
			break;
		}
	}
	return outputSession.processed();
}
